Edit or run this notebook

Using JavaScript inside Pluto

You have already seen that Pluto is designed to be interactive. You can make fantastic explorable documents using just the basic inputs provided by PlutoUI, together with the wide range of visualization libraries that Julia offers.

However, if you want to take your interactive document one step further, then Pluto offers a great framework for combining Julia with HTML, CSS and JavaScript.

18.1 μs
3.4 s

Prerequisites

This document assumes that you have used HTML, CSS and JavaScript before in another context. If you know Julia, and you want to add these web languages to your skillset, we encourage you to do so! It will be useful knowledge, also outside of Pluto.

5.2 μs

Learning HTML and CSS

It is easy to learn HTML and CSS because they are not 'programming languages' like Julia and JavaScript, they are markup languages: there are no loops, functions or arrays, you only declare how your document is structured (HTML) and what that structure looks like on a 2D color display (CSS).

As an example, this is what this cell looks like, written in HTML and CSS:

2.2 μs
Show with syntax highlighting
8.3 ms

My personal favourite resource for learning HTML and CSS is the Mozilla Developer Network (MDN).

-fons

3.1 μs

Learning JavaScript

After learning HTML and CSS, you can already spice up your Pluto notebooks but creating custom layouts, generated dynamically from Julia data. To take things to the next level, you can learn JavaScript. We recommend using an online resource for this.

My personal favourite is javascript.info, a high-quality, open source tutorial. I use it too!

-fons

It is hard to say whether it is easy to learn JavaScript using Pluto. On one hand, we highly recommend the high-quality public learning material that already exists for JavaScript, which is generally written in the context of writing traditional web apps. On the other hand, if you have a specific Pluto-related project in mind, then this could be a great motivator to continue learning!

A third option is to learn JavaScript using observablehq.com, an online reactive notebook for JavaScript (it's awesome!). Pluto's JavaScript runtime is designed to be very close to the way you write code in observable, so the skills you learn there will be transferable!

If you chose to learn JavaScript using Pluto, let me know how it went, and how we can improve! fons@plutojl.org

14.4 μs
200 ns

Essentials

Custom @bind output

10.4 μs

You can use JavaScript to write input widgets. The input event can be triggered on any object using

obj.value = ...
obj.dispatchEvent(new CustomEvent("input"))

For example, here is a button widget that will send the number of times it has been clicked as the value:

8.1 μs
ClickCounter (generic function with 2 methods)
32.3 μs
Show with syntax highlighting
9.1 μs
18.0 ms
missing
100 ns

As an exercise to get familiar with these techniques, you can try the following:

  • 👉 Add a "reset to zero" button to the widget above.

  • 👉 Make the bound value an array that increases size when you click, instead of a single number.

  • 👉 Create a "two sliders" widget: combine two sliders (<input type=range>) into a single widget, where the bound value is the two-element array with both values.

  • 👉 Create a "click to send" widget: combine a text input and a button, and only send the contents of the text field when the button is clicked, not on every keystroke.

Questions? Ask them on our GitHub Discussions!

20.9 μs

Debugging

The HTML, CSS and JavaScript that you write run in the browser, so you should use the browser's built-in developer tools to debug your code.

8.0 μs
2.3 μs
Can you find out which CSS class this is?
7.9 ms

Selecting elements

When writing the javascript code for a widget, it is common to select elements inside the widgets to manipulate them. In the number-of-clicks example above, we selected the <div> and <button> elements in our code, to trigger the input event, and attach event listeners, respectively.

There are a numbers of ways to do this, and the recommended strategy is to create a wrapper <div>, and use currentScript.parentElement to select it.

currentScript

When Pluto runs the code inside <script> tags, it assigns a reference to that script element to a variable called currentScript. You can then use properties like previousElementSibling or parentElement to "navigate to other elements".

Let's look at the "wrapper div strategy" again.

@htl("""

<!-- the wrapper div -->
<div>

    <button id="first">Hello</button>
    <button id="second">Julians!</button>
    
    <script>
        var wrapper_div = currentScript.parentElement
        // we can now use querySelector to select anything we want
        var first_button = wrapper_div.querySelector("button#first")

        console.log(first_button)
    </script>
</div>
""")
15.5 μs

Why not just select on document.body?

In the example above, it would have been easier to just select the button directly, using:

// ⛔ do no use:
var first_button = document.body.querySelector("button#first")

However, this becomes a problem when combining using the widget multiple times in the same notebook, since all selectors will point to the first instance.

Similarly, try not to search relative to the <pluto-cell> or <pluto-output> element, because users might want to combine multiple instances of the widget in a single cell.

16.6 μs

Interpolation

Julia has a nice feature: string interpolation:

7.5 μs
who
"🌍"
200 ns
"Hello 🌍!"
1.4 μs

With some (frustrating) exceptions, you can also interpolate into Markdown literals:

3.4 μs

Hello 🌍!

9.6 μs

However, you cannot interpolate into an html" string:

5.5 μs

Hello $(who)!

2.3 μs

😢 For this feature, we highly recommend the new package HypertextLiteral.jl, which has an @htl macro that supports interpolation:

Interpolating into HTML – HypertextLiteral.jl

8.6 μs

Hello 🌍!

1.7 ms

It has a bunch of very cool features! Including:

  • Interpolate any HTML-showable object, such as plots and images, or another @htl literal.

  • Interpolated lists are expanded (like in this cell!).

  • Easy syntax for CSS
59.2 ms
cool_features
144 ms

Why not just HTML(...)?

You might be thinking, why don't we just use the HTML function, together with string interpolation? The main problem is correctly handling HTML escaping rules. For example:

7.8 μs
cool_elements
"<div> and <marquee>"
200 ns
My favourite HTML elements are
and !
2.5 μs
My favourite HTML elements are <div> and <marquee>!
9.9 μs

Interpolating into JS – JSON.jl

Using HypertextLiteral.jl, we can interpolate objects into HTML output, great! Next, we want to interpolate data into scripts. The easiest way to do so is to use JSON.jl, in combination with string interpolation:

12.1 μs
my_data
3.2 μs
22.2 ms
CoolAwesomeFantastic!
280 ms
Show with syntax highlighting
4.7 μs

Script loading

To use external javascript dependencies, you can load them from a CDN, such as:

Just like when writing a browser app, there are two ways to import JS dependencies: a <script> tag, and the more modern ES6 import.

Loading method 1: ES6 imports

we recommend that you use an ES6 import if the library supports it.

Awkward note about syntax

Normally, you can import libraries inside JS using the import syntax:

import confetti from 'https://cdn.skypack.dev/canvas-confetti'
import { html, render, useEffect } from "https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs"

In Pluto, this is currently not yet supported, and you need to use a different syntax as workaround:

const { default: confetti } = await import("https://cdn.skypack.dev/canvas-confetti@1")
const { html, render, useEffect } = await import( "https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs")
23.3 μs
100 ns

Loading method 2: script tag

<script src="..."> tags with a src attribute set, like this tag to import the d3.js library:

<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>

will work as expected. The execution of other script tags within the same cell is delayed until a src script finished loading, and Pluto will make sure that every source file is only loaded once.

11.4 μs

Advanced

3.7 μs

Script output & observablehq/stdlib

Pluto's original inspiration was observablehq.com, and online reactive notebook for JavaScript. (It's REALLY good, try it out!) We design Pluto's JavaScript runtime to be close to the way you write code in observable.

Read more about the observable runtime in their (interactive) documentation. The following is also true for JavaScript-inside-scripts in Pluto:

  • ⭐️ If you return an HTML node, it will be displayed.

  • ⭐️ The observablehq/stdlib library is pre-imported, you can use DOM, html, Promises, etc.

  • ⭐️ When a cell re-runs reactively, this will be set to the previous output (with caveat, see the later section)

  • The variable invalidation is a Promise that will get resolved when the cell output is changed or removed. You can use this to remove event listeners, for example.

  • You can use top-level await, and a returned HTML node will be displayed when ready.

  • Code is run in "strict mode", use let x = 1 instead of x = 1.

The following is different in Pluto:

  • JavaScript code is not reactive, there are no global variables.

  • Cells can contain multiple script tags, and they will run consecutively (also when using await)

  • We do not (yet) support async generators, i.e. yield.

  • We do not support the observable keywords viewof and mutable.

32.1 μs

Example:

3.2 μs
  • Frances Ha by Noah Baumbach (2012)
  • Portrait de la jeune fille en feu by Céline Sciamma (2019)
  • De noorderlingen by Alex van Warmerdam (1992)
96.0 ms
Show with syntax highlighting
4.4 μs
1.5 μs

Stateful output with this

Just like in observablehq, if a cell re-runs reactively, then the javascript variable this will take the value of the last thing that was returned by the script. If the cell runs for the first time, then this == undefined. In particular, if you return an HTML node, and the cell runs a second time, then you can access the HTML node using this. Two reasons for using this feature are:

  • Stateful output: you can persist some state inbetween re-renders.

  • Performance: you can 'recycle' the previous DOM and update it partially (using d3, for example). When doing so, Pluto guarantees that the DOM node will always be visible, without flicker.

'Re-runs reactively'?

With this, we mean that the Julia cell re-runs not because of user input (Ctrl+S, Shift+Enter or clicking the play button), but because it was triggered by a variable reference.

☝️ Caveat

This feature is only enabled for <script> tags with the id attribute set, e.g. <script id="first">. Think of setting the id attribute as saying: "I am a Pluto script". There are two reasons for this:

  • One Pluto cell can output multiple scripts, Pluto needs to know which output to assign to which script.

  • Some existing scripts assume that this is set to window in toplevel code (like in the browser). By hiding the this-feature behind this caveat, we still support libraries that output such scripts.

22.6 μs
trigger
"edit me!"
200 ns
I am running for the first time!
2.3 ms
Show with syntax highlighting
4.6 μs

Example: d3.js transitions

Type the coordinates of the circles here!

4.8 μs
34.9 ms
dot_positions
173 ms
200 ns

Notice that, even though the cell below re-runs, we smoothly transition between states. We use this to maintain the d3 transition states inbetween reactive runs.

8.6 μs
18.9 ms
Show with syntax highlighting
4.6 μs

Example: Preact with persistent state

3.5 μs

Modify x, add and remove elements, and notice that preact maintains its state.

4.5 μs
x
5.2 μs
Hello world!
  • hello pluton!
  • 232000
  • 2
  • 2
  • 12
  • 12
  • 2
  • 21
  • 1
  • 2
  • 120000
98.3 ms
Show with syntax highlighting
4.6 μs
state
4.6 μs

Manipulate the editor itself

3.4 μs
100 ns

Appendix

4.7 μs
details (generic function with 2 methods)
38.1 μs
Loading...i